package dataTypes;

import helperClasses.BitSetHelper;
import helperClasses.Help;

import java.util.BitSet;

/**
 * High performance sudoku.
 * 
 * @author Felix Schliephacke
 * 
 */
public class SudokuBitSetImpl implements Cloneable {

	public static SudokuBitSetImpl getInstance(final int squareSize) {
		return new SudokuBitSetImpl(squareSize);
	}

	public static SudokuBitSetImpl getInstance(final int[][] sudoku) {
		return new SudokuBitSetImpl(sudoku);
	}

	public static SudokuBitSetImpl getInstance(final Integer[][] sudoku) {
		return new SudokuBitSetImpl(sudoku);
	}

	public static SudokuBitSetImpl getInstance(final Sudoku<Integer> sudoku) {
		return new SudokuBitSetImpl(sudoku);
	}

	public static SudokuBitSetImpl getInstance(final SudokuBitSetImpl sudoku) {
		return new SudokuBitSetImpl(sudoku);
	}

	private transient BitSet fields;

	private transient final int size;

	private transient final int squareSize;

	private SudokuBitSetImpl(final int squareSize) {
		this.squareSize = squareSize;
		this.size = this.squareSize * this.squareSize;
		this.fields = new BitSet(this.size * this.size * this.size);
		this.fields.set(0, this.size * this.size * this.size);
	}

	private SudokuBitSetImpl(final int[][] sudoku) {
		this.size = sudoku.length;
		this.squareSize = (int) Math.sqrt(this.size);
		this.fields = new BitSet(this.size * this.size * this.size);
		this.fields.set(0, this.size * this.size * this.size);
		for (int i = 0; i < this.size; i++) {
			for (int j = 0; j < this.size; j++) {
				if (sudoku[i][j] > 0 && sudoku[i][j] <= this.size) {
					this.setField(i, j, sudoku[i][j]);
				}
			}
		}
	}

	private SudokuBitSetImpl(final Integer[][] sudoku) {
		this.size = sudoku.length;
		this.squareSize = (int) Math.sqrt(this.size);
		this.fields = new BitSet(this.size * this.size * this.size);
		this.fields.set(0, this.size * this.size * this.size);
		for (int i = 0; i < this.size; i++) {
			for (int j = 0; j < this.size; j++) {
				if (sudoku[i][j] != null & sudoku[i][j] > 0
						&& sudoku[i][j] <= this.size) {
					this.setField(i, j, sudoku[i][j]);
				}
			}
		}
	}

	private SudokuBitSetImpl(final Sudoku<Integer> sudoku) {
		this.size = sudoku.getSize();
		this.squareSize = sudoku.getSquareSize();
		this.fields = new BitSet(this.size * this.size * this.size);
		for (int row = 0; row < this.size; row++) {
			for (int column = 0; column < this.size; column++) {
				this.setField(row, column, sudoku.getField(row, column));
			}
		}
	}

	private SudokuBitSetImpl(final SudokuBitSetImpl sudoku) {
		this.size = sudoku.getSize();
		this.squareSize = sudoku.getSquareSize();
		this.fields = (BitSet) sudoku.fields.clone();
	}

	@Override
	public SudokuBitSetImpl clone() {
		return new SudokuBitSetImpl(this);
	}

	public Sudoku<Integer> convert() {
		return SudokuImpl.getInstance(this.getFieldArray());
	}

	@Override
	public void finalize() {
		this.fields = null;
	}

	public BitSet getBitSetColumn(final int column) {
		final BitSet temp = new BitSet(this.size * this.size);
		int count = 0;
		for (int i = 0; i < this.size; i++) {
			final int[] indices = BitSetHelper.calculateIndices(i, column,
					this.size);
			for (final int index : indices) {
				temp.set(count++, this.fields.get(index));
			}
		}
		return temp;
	}

	public BitSet getBitSetField(final int row, final int column) {
		final int startIndex = row * this.size * this.size + column * this.size;
		return this.fields.get(startIndex, startIndex + this.size);
	}

	public BitSet getBitSetRow(final int row) {
		final int startIndex = row * this.size * this.size;
		return this.fields.get(startIndex, startIndex + this.size * this.size);
	}

	public BitSet getBitSetSquare(final int square) {
		final BitSet temp = new BitSet(this.size * this.size);
		final int[] rows = Help.calculateRowIndices(square, this.squareSize);
		final int[] columns = Help.calculateColumnIndices(square,
				this.squareSize);
		int[] indices;
		int count = 0;
		for (final int row : rows) {
			for (final int column : columns) {
				indices = BitSetHelper.calculateIndices(row, column, this.size);
				for (final int index : indices) {
					temp.set(count++, this.fields.get(index));
				}
			}
		}
		return temp;
	}

	public int[][] getColumn(final int column) throws IllegalArgumentException {
		final int[][] temp = new int[this.size][this.size];
		for (int row = 0; row < this.size; row++) {
			temp[row] = this.getField(row, column);
		}
		return temp;
	}

	public int[] getField(final int row, final int column)
			throws IllegalArgumentException {
		if (row >= this.size || column >= this.size) {
			throw new IllegalArgumentException(
					"Row or column index out of range.");
		}
		final int startIndex = column * this.size + row * this.size * this.size;
		final int stopIndex = startIndex + this.size;
		final int[] temp = new int[this.size];
		for (int i = startIndex, arrayCount = 0; i < stopIndex; i++) {
			if (this.fields.get(startIndex + i)) {
				temp[arrayCount] = ++arrayCount;
			}
		}
		return temp;
	}

	private Integer[][] getFieldArray() {
		final Integer[][] temp = new Integer[this.size][this.size];
		for (int i = 0; i < this.size; i++) {
			for (int j = 0; j < this.size; j++) {
				final BitSet field = this.getBitSetField(i, j);
				if (field.cardinality() == 1) {
					temp[i][j] = field.nextSetBit(0) + 1;
				} else {
					temp[i][j] = null;
				}
			}
		}
		return temp;
	}

	public int[][] getRow(final int row) throws IllegalArgumentException {
		final int[][] temp = new int[this.size][this.size];
		for (int i = 0; i < this.size; i++) {
			temp[i] = this.getField(row, i);
		}
		return temp;
	}

	public int getSize() {
		return this.size;
	}

	public int[][] getSquare(final int index) throws IllegalArgumentException {
		final int[][] square = new int[this.size][this.size];
		final int[] rows = Help.calculateRowIndices(index, this.squareSize);
		final int[] columns = Help.calculateColumnIndices(index,
				this.squareSize);
		final int tempSquareIndex = 0;
		for (final int i : rows) {
			for (final int j : columns) {
				square[tempSquareIndex] = this.getField(i, j);
			}
		}
		return square;
	}

	public int getSquareSize() {
		return this.squareSize;
	}

	public boolean isSolvable() {
		for (int row = 0; row < this.size; row++) {
			for (int column = 0; column < this.size; column++) {
				if (this.getBitSetField(row, column).cardinality() == 0) {
					return false;
				}
			}
		}
		return true;
	}

	public boolean isSolved() {
		if (this.fields.cardinality() != this.size) {
			return false;
		}
		BitSet tempRow = new BitSet(this.size * this.size);
		BitSet tempColumn = new BitSet(this.size * this.size);
		BitSet tempSquare = new BitSet(this.size * this.size);
		final BitSet row = new BitSet(this.size * this.size);
		final BitSet column = new BitSet(this.size * this.size);
		final BitSet square = new BitSet(this.size * this.size);
		for (int i = 0; i < this.size; i++) {
			tempRow = this.getBitSetRow(i);
			tempColumn = this.getBitSetColumn(i);
			tempSquare = this.getBitSetSquare(i);
			row.xor(tempRow);
			column.xor(tempColumn);
			square.xor(tempSquare);
			if (tempRow.cardinality() != 1 || tempColumn.cardinality() != 1
					|| tempSquare.cardinality() != 1
					|| row.cardinality() != i + 1
					|| column.cardinality() != i + 1
					|| square.cardinality() != i + 1) {
				return false;
			}
		}
		return true;
	}

	private boolean removeFromColumn(final int row, final int column,
			final int toRemove) {
		boolean removed = false;
		for (int i = 0; i < this.size; i++) {
			if (i != row && this.removeFromField(i, column, toRemove)) {
				removed = true;
			}
		}
		return removed;
	}

	public boolean removeFromContext(final int row, final int column,
			final int toRemove) {
		return this.removeFromRow(row, column, toRemove)
				| this.removeFromColumn(row, column, toRemove)
				| this.removeFromSquare(row, column, toRemove);
	}

	public boolean removeFromField(final int row, final int column,
			final int arg) throws IllegalArgumentException {
		final int index = this.size * this.size * row + this.size * column
				+ arg - 1;
		final boolean removed = this.fields.get(index);
		this.fields.clear(index);
		return removed;
	}

	private boolean removeFromRow(final int row, final int column,
			final int toRemove) {
		boolean removed = false;
		for (int i = 0; i < this.size; i++) {
			if (i != column && this.removeFromField(row, i, toRemove)) {
				removed = true;
			}
		}
		return removed;
	}

	private boolean removeFromSquare(final int row, final int column,
			final int toRemove) {
		boolean removed = false;
		final int squareIndex = Help.calculateSquareIndex(row, column,
				this.squareSize);
		final int[] rows = Help.calculateRowIndices(squareIndex,
				this.squareSize);
		final int[] columns = Help.calculateColumnIndices(squareIndex,
				this.squareSize);
		for (final int i : rows) {
			for (final int j : columns) {
				if ((i != row || j != column)
						&& this.removeFromField(i, j, toRemove)) {
					removed = true;
				}
			}
		}
		return removed;
	}

	public void reset() {
		for (int row = 0; row < this.size; row++) {
			for (int column = 0; column < this.size; column++) {
				this.resetField(row, column);
			}
		}
	}

	public void resetColumn(final int column) throws IllegalArgumentException {
		for (int i = 0; i < this.size; i++) {
			this.resetField(i, column);
		}
	}

	public void resetField(final int row, final int column)
			throws IllegalArgumentException {
		final int startIndex = row * this.size * this.size + column * this.size;
		this.fields.set(startIndex, startIndex + this.size);
	}

	public void resetRow(final int row) throws IllegalArgumentException {
		for (int i = 0; i < this.size; i++) {
			this.resetField(row, i);
		}
	}

	public void resetSquare(final int square) {
		final int[] rows = Help.calculateRowIndices(square, this.squareSize);
		final int[] columns = Help.calculateColumnIndices(square,
				this.squareSize);
		for (final int row : rows) {
			for (final int column : columns) {
				this.resetField(row, column);
			}
		}
	}

	private void setField(final int row, final int column,
			final Field<Integer> content) {
		final int[] indices = BitSetHelper.calculateIndices(row, column,
				this.size);
		for (final Integer i : content) {
			this.fields.set(indices[i - 1]);
		}
	}

	public void setField(final int row, final int column, final int content) {
		if (content < 1 || content > this.size) {
			throw new IllegalArgumentException();
		}
		final int startIndex = this.size * this.size * row + this.size * column;
		this.fields.clear(startIndex, startIndex + this.size);
		this.fields.set(startIndex + content - 1);
	}

	@Override
	public String toString() {
		final StringBuffer buffer = new StringBuffer();
		BitSet field;
		for (int row = 0; row < this.size; row++) {
			for (int column = 0; column < this.size; column++) {
				field = this.getBitSetField(row, column);
				if (field.cardinality() == 1) {
					for (int i = 0; i < this.size; i++) {
						if (field.get(i)) {
							buffer.append(i + 1);
							buffer.append("   ");
							break;
						}
					}
				} else {
					buffer.append("(");
					buffer.append(field.cardinality());
					buffer.append(") ");
				}
			}
			buffer.append("\n");
		}
		return buffer.toString();
	}
}
